package com.limespot.api.repl;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.limespot.api.User;
import com.limespot.api.misc.AbstractMain;

public class Main extends AbstractMain {
	
	/** Means no command was executed -- e.g. 'help' */
	final static Object NO_OP = new Object();
	
	/** Means to quit. */
	final static Object QUIT = new Object();
	
	private Object cur = null;

	public void realMain() {
		
		cur = getUser();

		while (true) {
			
			if (cur != null) out(cur);
			out("> ");
			Menu m = showMenu();
			BufferedReader prompt = new BufferedReader(new InputStreamReader(System.in));
			String cmd = null;
			try {
				cmd = prompt.readLine();
			} catch (IOException e) {
				e.printStackTrace();
			}
			if (cmd == null) continue;
			Object o = m.invoke(cmd);
			if (o == null) {
				
			} else if (o == NO_OP) {
				
			} else if (o == QUIT) {
				outln("quitting...");
				break;
			} else {
				printObject(o);
				cur = o;
			}
		}
	}
	
	interface Invokable {
		Object invoke() throws IllegalArgumentException, IllegalAccessException, InvocationTargetException;
		String description();
	}
	
	private class Menu {
			
		private final Map<String,Invokable> names2invokables = new HashMap<String,Invokable>();

		private abstract class I implements Invokable {
			final String s;
			I(String s) {this.s = s;}
			public String description() {return s;}
		}					
		
				
		Menu() {
			addCommand("help", new I("print this message") {
				public Object invoke() throws IllegalArgumentException, IllegalAccessException,
						InvocationTargetException {
					showHelp();
					return null;
				}
			});
			addCommand("quit", new I("quit this application") {
				public Object invoke() throws IllegalArgumentException, IllegalAccessException,
						InvocationTargetException {
					return QUIT;
				}
			});
			addCommand("print", new I("print this object") {
				public Object invoke() throws IllegalArgumentException, IllegalAccessException,
						InvocationTargetException {
					return cur;
				}
			});
			
		}
		
		void addCommand(String name, final Method method) {
			Invokable inv = new I("invoke " + getName(method.getDeclaringClass()) + "." + method.getName() + "()") {
				public Object invoke() throws IllegalArgumentException, IllegalAccessException,
						InvocationTargetException {
					method.setAccessible(true);
					return method.invoke(cur,new Object[0]);
				}
			};
			addCommand(name,inv);
		}
		
		void addCommand(String name, Invokable inv) {
			names2invokables.put(name.toLowerCase(),inv);
		}
		
		Object invoke(String cmd) {
			String lc = cmd.toLowerCase();

			// Try to find the closest match
			// TODO: we could build a trie for this
			List<String> possibleMatches = new ArrayList<String>();
			for (Map.Entry<String,Invokable> e : names2invokables.entrySet()) {
				String name = e.getKey();
				if (name.startsWith(lc)) {
					possibleMatches.add(name);
				}
			}
			
			// Either show a menu with the possible choices or execute the one method
			if (possibleMatches.size() == 0) {
				outln("No match found for " + cmd + ", type 'help' to see possible commands");
			} else if (possibleMatches.size() == 1) {
				Invokable inv = names2invokables.get(possibleMatches.get(0));
				try {
					return inv.invoke();
				} catch (IllegalArgumentException e1) {
					e1.printStackTrace();
				} catch (IllegalAccessException e1) {
					e1.printStackTrace();
				} catch (InvocationTargetException e1) {
					e1.printStackTrace();
				}
			} else {
				outln("Too many matches:");
				for (String n : possibleMatches) {
					outln(" - " + n);
				}
			}
			return null;
		}
		
		void showHelp() {
			outln("Possible commands are:");
			List<String> names = new ArrayList<String>(names2invokables.keySet());
			Collections.sort(names);
			int max = Integer.MIN_VALUE;
			for (String name : names) {
				max = Math.max(max,name.length());
			}
			for (String name : names) {
				String msg = " - " + name + " ";
				for (int i=name.length(); i<max; i++) {
					msg += " ";
				}
				msg += "   [" + names2invokables.get(name).description() + "]";
				outln(msg);
			}
		}
	}
	
	/**
	 * Create a menu with all the get methods.
	 */
	private Menu showMenu() {
		final Menu res = new Menu();
		fillMenu(res,cur.getClass());
		return res;
	}
	
	/**
	 * Recurs on the superclasses of cls and fills <code>res</code> with its methods.
	 * 
	 * @param res menu to fill
	 * @param cls class from which to start recursion
	 */
	@SuppressWarnings("unchecked")
	private void fillMenu(Menu res, Class cls) {
		Method[] ms = cls.getMethods();
		for (Method m : ms) {
			if (m.getParameterTypes().length != 0) continue;
			String name = m.getName();
			if (!name.startsWith("get")) continue;
			String lc = name.toLowerCase().substring(3);
			res.addCommand(lc,m);
		}
		if (!cls.equals(Object.class)) {
			fillMenu(res,cls.getSuperclass());
		}
	}
	
	private void out(Object msg) {
		System.out.print(msg);
		System.out.flush();
	}
	
	private void outln(Object msg) {
		System.out.println(msg);
		System.out.flush();
	}
	
	private void printObject(Object p) {
		printObject(p,"");
	}
	
	@SuppressWarnings("unchecked")
	private void printObject(Object p, String tab) {
		if (p.getClass().isArray()) {
			int len = Array.getLength(p);
			for (int i=0; i<len; i++) {
				out(tab + "[" + i + "]");
				printObject(Array.get(p,i),tab+" ");
			}
		} else if (p instanceof Collection) {
			Collection<Object> v = (Collection<Object>)p;
			for (Object o : v) printObject(o,tab+" ");
		} else if (p instanceof Iterable) {
			Iterable<Object> v = (Iterable<Object>)p;
			for (Object o : v) printObject(o,tab+" ");
		} else {
			outln(p);
		}
	}
	
	@SuppressWarnings("unchecked")
	private final static Map<Class,String> classes2names = new HashMap<Class,String>();
	
	@SuppressWarnings("unchecked")
	private static String getName(Class cls) {
		
		// Look for the cached version
		String res = classes2names.get(cls);
		if (res != null) return res;
		
		// Look for the short version
		String[] exts = {
				"java.lang",
				User.class.getPackage().getName(),
		};
		final String pkg = cls.getPackage().getName();
		for (String ext : exts) {
			if (ext.equals(pkg)) {
				String name = cls.getName();
				int ilastDot = name.lastIndexOf('.');
				classes2names.put(cls,name.substring(ilastDot+1));
				return name;
			}
		}
		
		// Use the normal version
		String name = cls.getName();
		classes2names.put(cls,name);
		return name;
	}
	
	public static void main(String[] args) {
		new Main().realMain(args);
	}


}
